﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

using TracedReality;
using TracedReality.Primitive;
using TracedReality.RayTracing;
using TracedReality.SpatialTrees;

using DrawingExtensions;

namespace Interface.Visualization
{
    public partial class Visualizer : Form
    {
        private bool loaded;
        private GLCamera camera;
        private Scene scene;
        private CullingTree cullTree;
        private BoundingBox bound;
        private Box boundingbox;

        private Matrix4d perspectiveMatrix;
        private Vector3d translation = new Vector3d();
        private Perspective perspective;
        private ViewFrustum frustum;

        private Shader shader;

        public Visualizer(ref Scene s)
        {
            InitializeComponent();

            loaded = false;
            scene = s;
            cullTree = new CullingTree(scene.getBoundingBox());
            foreach (Shape sh in scene.shapes) { cullTree.Add(sh); }

            // If there are too few shapes then using a BVH is actually detrimental
            // 100 is basically just an arbitrary choice.
            if (scene.shapes.Count > 100) { cullTree.Split(Settings.maxSDSNodeCount, Settings.maxSDSDepth); }
            
            Material black = new Material(new Vector());
            bound = new BoundingBox(scene.getBoundingBox());
            boundingbox = new Box(bound.min, bound.size, ref black);

            bound.extend(bound.max + 0.5*bound.size);
            bound.extend(bound.min - 0.5*bound.size);

            // OpenGL wants the near and far planes to be positive
            if (bound.min.z < 0)
            {
                translation = new Vector3d(0, 0, -bound.min.z + 1);
                bound.max.z += -bound.min.z + 1;
                bound.min.z = 1;
            }

            double aspect = (double)glControl1.Width / (double)glControl1.Height;
            double asp_angle = scene.camera.angle;
            if (aspect > 1) { asp_angle /= aspect; }
            perspectiveMatrix = Matrix4d.Perspective(asp_angle, aspect, bound.min.z, bound.max.z);
            perspective = new Perspective()
            {
                fov = asp_angle,
                aspect = aspect,
                near = bound.min.z,
                far = bound.max.z
            };
            frustum = new ViewFrustum(perspective);
            frustum.SetTranslation(new Vector(translation.X, translation.Y, translation.Z));

            Vector3d cp = new Vector3d(s.camera.position.x, s.camera.position.y, s.camera.position.z);
            Vector3d cd = new Vector3d(s.camera.pointOfInterest.x, s.camera.pointOfInterest.y, s.camera.pointOfInterest.z) - cp;
            cd.Normalize();
            Vector3d cu = new Vector3d(s.camera.up.x, s.camera.up.y, s.camera.up.z);
            camera = new GLCamera(cp, cd, cu);
            frustum.SetCamera(camera);

            label1.Text = String.Format("Camera stats:\nPosition: {0}\nDirection: {1}\nUp: {2}", niceVector3dToString(camera.Position), niceVector3dToString(camera.Direction), niceVector3dToString(camera.Up));
        }

        private void setupCamera()
        {
            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadIdentity();
            GL.MultMatrix(ref perspectiveMatrix);
            Matrix4d cam = Matrix4d.LookAt(camera.Position, camera.Interest, camera.Up);
            GL.MultMatrix(ref cam);
        }

        private void setupShaders(int numLights)
        {
            string vert;
            string frag;

            vert = "#define NUMLIGHTS " + numLights + @"
                uniform vec3 eye;

                varying vec3 norm;
                varying vec3 lDir[NUMLIGHTS];
                varying vec3 hVecs[NUMLIGHTS];

                void main() {
                    norm = normalize(gl_NormalMatrix * gl_Normal);
                    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

                    vec4 vert = gl_ModelViewMatrix * gl_Vertex;
                    for(int l = 0; l < NUMLIGHTS; l++) {
                        lDir[l] = vec3(gl_LightSource[l].position - vert);
                        hVecs[l] = normalize(gl_LightSource[l].halfVector.xyz);
                    }
                }";

            frag = "#define NUMLIGHTS " + numLights + @"
                varying vec3 norm;
                varying vec3 lDir[NUMLIGHTS];
                varying vec3 hVecs[NUMLIGHTS];

                void main() {
                    vec4 color = gl_FrontLightModelProduct.sceneColor;
                    vec3 normal = normalize(norm);
                    
                    for(int l = 0; l < NUMLIGHTS; l++) {
                        vec3 lightDir = normalize(lDir[l]);
                        vec3 halfVec = normalize(hVecs[l]);

                        float NdotL = max(0.0, dot(normal, lightDir));
                        color += NdotL * gl_FrontLightProduct[l].diffuse;

                        if(NdotL > 0) {
                            float NdotHV = max(0.0, dot(normal, halfVec));
                            color += pow(NdotHV, gl_FrontMaterial.shininess) * gl_FrontLightProduct[l].specular;
                        }
                    }

                    gl_FragColor = color;
                    gl_FragColor.a = 1.0;
                }";

            shader = new Shader(vert, frag);
        }

        private void glControl1_Load(object sender, EventArgs e)
        {
            GL.Viewport(0, 0, glControl1.Width, glControl1.Height);

            setupShaders(Math.Min(8, scene.lights.Count));
            
            GL.ClearColor((Color)Settings.backColor);
            
            GL.Enable(EnableCap.DepthTest);

            GL.Enable(EnableCap.Lighting);
            GL.ShadeModel(ShadingModel.Smooth);
            GL.LightModel(LightModelParameter.LightModelAmbient, (float[])Settings.ambientColor);
            GL.LightModel(LightModelParameter.LightModelLocalViewer, 1.0f);

            loaded = true;
        }

        private LightName[] lightNames = { LightName.Light0, LightName.Light1, LightName.Light2, LightName.Light3, LightName.Light4, LightName.Light5, LightName.Light6, LightName.Light7 };
        private EnableCap[] lightEnables = { EnableCap.Light0, EnableCap.Light1, EnableCap.Light2, EnableCap.Light3, EnableCap.Light4, EnableCap.Light5, EnableCap.Light6, EnableCap.Light7 };
        private void setupLights()
        {
            float[] ambient = new float[] { 0, 0, 0, 0 };
            for (int a = 0; a < 8 && a < scene.lights.Count; a++)
            {
                LightName name = lightNames[a];
                Light l = scene.lights[a];
                float[] color = (float[])l.color;
                float[] position = new float[] { 0, 0, 0, 0 };

                if (l is PointLight)
                {
                    PointLight m = (PointLight)l;
                    position = (float[])m.position;
                }
                else if (l is AreaLight)
                {
                    AreaLight m = (AreaLight)l;
                    position = (float[])m.center;
                }

                GL.Light(name, LightParameter.Ambient, ambient);
                GL.Light(name, LightParameter.Diffuse, color);
                GL.Light(name, LightParameter.Specular, color);
                GL.Light(name, LightParameter.Position, position);
                GL.Enable(lightEnables[a]);
            }
        }

        private void glControl1_Paint(object sender, PaintEventArgs e)
        {
            if (!loaded) { return; }

            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            setupCamera();
            
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadIdentity();

            setupLights();

            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
            boundingbox.Draw(1);

            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Fill);
            shader.Activate();

            Vector cam = new Vector(camera.Position.X, camera.Position.Y, camera.Position.Z);
            
            List<Shape> drawnShapes = cullTree.GetShapesInFrustum(frustum);
            // Workaround while culling doesn't work - draw everything anyways
            drawnShapes = scene.shapes;
            foreach (Shape s in drawnShapes)
            {
                // Distance for LoD
                double distance = Vector.Distance(cam, s.bound.center);
                s.Draw(distance);
            }
            shader.Deactivate();

            glControl1.SwapBuffers();
        }

        private string niceVector3dToString(Vector3d v)
        {
            return string.Format("[{0:0.000}, {1:0.000}, {2:0.000}]", v.X, v.Y, v.Z);
        }

        private void glControl1_KeyDown(object sender, KeyEventArgs e)
        {
            if (!loaded) { return; }

            switch (e.KeyCode)
            {
                case Keys.D: camera.MoveSide(-0.1f); break;
                case Keys.A: camera.MoveSide( 0.1f); break;
                case Keys.S: camera.MoveIn(  -0.1f); break;
                case Keys.W: camera.MoveIn(   0.1f); break;
                case Keys.F: camera.MoveUp(  -0.1f); break;
                case Keys.R: camera.MoveUp(   0.1f); break;
                case Keys.J: camera.Yaw(   MathHelper.Pi / 48f); break;
                case Keys.L: camera.Yaw(  -MathHelper.Pi / 48f); break;
                case Keys.I: camera.Pitch(-MathHelper.Pi / 48f); break;
                case Keys.K: camera.Pitch( MathHelper.Pi / 48f); break;
                case Keys.O: camera.Roll(  MathHelper.Pi / 48f); break;
                case Keys.U: camera.Roll( -MathHelper.Pi / 48f); break;
                case Keys.Escape: this.Close(); break;
            }
            frustum.SetCamera(camera);

            label1.Text = String.Format("Camera stats:\nPosition: {0}\nDirection: {1}\nUp: {2}", niceVector3dToString(camera.Position), niceVector3dToString(camera.Direction), niceVector3dToString(camera.Up));
            
            glControl1.Invalidate();
        }

        private void glControl1_Resize(object sender, EventArgs e)
        {
            if (!loaded) { return; }

            GL.Viewport(0, 0, glControl1.Width, glControl1.Height);
            glControl1.Invalidate();
        }

        private void glControl1_MouseDown(object sender, MouseEventArgs e)
        {
            
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (!loaded) { return; }

            scene.camera.position = new Vector(camera.Position.X, camera.Position.Y, camera.Position.Z);
            scene.camera.pointOfInterest = new Vector(camera.Interest.X, camera.Interest.Y, camera.Interest.Z);
            scene.camera.up = new Vector(camera.Up.X, camera.Up.Y, camera.Up.Z);
        }
    }
}
